/*
 * Copyright (C) 2014 The Dagger Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dagger.internal.codegen.base;

import static com.google.auto.common.GeneratedAnnotations.generatedAnnotation;
import static com.google.common.base.Preconditions.checkNotNull;
import static dagger.internal.codegen.javapoet.AnnotationSpecs.Suppression.RAWTYPES;
import static dagger.internal.codegen.javapoet.AnnotationSpecs.Suppression.UNCHECKED;

import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import dagger.internal.codegen.javapoet.AnnotationSpecs;
import dagger.internal.codegen.javapoet.AnnotationSpecs.Suppression;
import dagger.internal.codegen.langmodel.DaggerElements;
import java.util.Optional;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;

/**
 * A template class that provides a framework for properly handling IO while generating source files
 * from an annotation processor. Particularly, it makes a best effort to ensure that files that fail
 * to write successfully are deleted.
 *
 * @param <T> The input type from which source is to be generated.
 */
public abstract class SourceFileGenerator<T> {
  private static final String GENERATED_COMMENTS = "https://dagger.dev";

  private final Filer filer;
  private final DaggerElements elements;
  private final SourceVersion sourceVersion;

  public SourceFileGenerator(Filer filer, DaggerElements elements, SourceVersion sourceVersion) {
    this.filer = checkNotNull(filer);
    this.elements = checkNotNull(elements);
    this.sourceVersion = checkNotNull(sourceVersion);
  }

  public SourceFileGenerator(SourceFileGenerator<T> delegate) {
    this(delegate.filer, delegate.elements, delegate.sourceVersion);
  }

  /**
   * Generates a source file to be compiled for {@code T}. Writes any generation exception to {@code
   * messager} and does not throw.
   */
  public void generate(T input, Messager messager) {
    try {
      generate(input);
    } catch (SourceFileGenerationException e) {
      e.printMessageTo(messager);
    }
  }

  /** Generates a source file to be compiled for {@code T}. */
  public void generate(T input) throws SourceFileGenerationException {
    ClassName generatedTypeName = nameGeneratedType(input);
    Optional<TypeSpec.Builder> type = write(generatedTypeName, input);
    if (!type.isPresent()) {
      return;
    }
    try {
      buildJavaFile(generatedTypeName, input, type.get()).writeTo(filer);
    } catch (Exception e) {
      // if the code above threw a SFGE, use that
      Throwables.propagateIfPossible(e, SourceFileGenerationException.class);
      // otherwise, throw a new one
      throw new SourceFileGenerationException(Optional.empty(), e, originatingElement(input));
    }
  }

  private JavaFile buildJavaFile(
      ClassName generatedTypeName, T input, TypeSpec.Builder typeSpecBuilder) {
    typeSpecBuilder.addOriginatingElement(originatingElement(input));
    Optional<AnnotationSpec> generatedAnnotation =
        generatedAnnotation(elements, sourceVersion)
            .map(
                annotation ->
                    AnnotationSpec.builder(ClassName.get(annotation))
                        .addMember("value", "$S", "dagger.internal.codegen.ComponentProcessor")
                        .addMember("comments", "$S", GENERATED_COMMENTS)
                        .build());
    generatedAnnotation.ifPresent(typeSpecBuilder::addAnnotation);

    // TODO(b/134590785): remove this and only suppress annotations locally, if necessary
    typeSpecBuilder.addAnnotation(
        AnnotationSpecs.suppressWarnings(
            ImmutableSet.<Suppression>builder()
                .addAll(warningSuppressions())
                .add(UNCHECKED, RAWTYPES)
                .build()));

    JavaFile.Builder javaFileBuilder =
        JavaFile.builder(generatedTypeName.packageName(), typeSpecBuilder.build())
            .skipJavaLangImports(true);
    if (!generatedAnnotation.isPresent()) {
      javaFileBuilder.addFileComment("Generated by Dagger ($L).", GENERATED_COMMENTS);
    }
    return javaFileBuilder.build();
  }

  /** Implementations should return the {@link ClassName} for the top-level type to be generated. */
  public abstract ClassName nameGeneratedType(T input);

  /** Returns the originating element of the generating type. */
  public abstract Element originatingElement(T input);

  /**
   * Returns a {@link TypeSpec.Builder type} to be generated for {@code T}, or {@link
   * Optional#empty()} if no file should be generated.
   */
  // TODO(ronshapiro): write() makes more sense in JavaWriter where all writers are mutable.
  // consider renaming to something like typeBuilder() which conveys the mutability of the result
  public abstract Optional<TypeSpec.Builder> write(ClassName generatedTypeName, T input);

  /** Returns {@link Suppression}s that are applied to files generated by this generator. */
  // TODO(b/134590785): When suppressions are removed locally, remove this and inline the usages
  protected ImmutableSet<Suppression> warningSuppressions() {
    return ImmutableSet.of();
  }
}
